/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { BaseChecker, BaseMetaData } from '../BaseChecker';
import { Defects, IssueReport, MatcherCallback, MatcherTypes, Rule, ViewTreeNodeMatcher } from '../../Index';
import {
    ArkAssignStmt,
    ArkField,
    ArkInstanceFieldRef,
    ArkMethod,
    Constant,
    Local,
    ModelUtils,
    Scene,
    Stmt,
    ViewTreeNode
} from 'arkanalyzer';
import { MethodSignature } from 'arkanalyzer/lib/core/model/ArkSignature';

const gMetaData: BaseMetaData = {
    severity: 3,
    ruleDocPath: 'docs/bad-lottie-opacity-check.md',
    description: 'Not recommended to use opacity for controlling the visibility of Lottie animations.'
};

const loadAnimationSignatures = new Set([
    '@thirdParty/@ohos/lottie/index.d.ts: LottiePlayer.loadAnimation(@built-in/lib.es5.d.ts: Object)',
    '@thirdParty/@ohos/lottie/index.d.ts: LottiePlayer.loadAnimation(@ES2015/BuiltinClass: Object)'
]);

export class BadLottieOpacityCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];

    private vtnMatcher: ViewTreeNodeMatcher = {
        matcherType: MatcherTypes.VIEW_TREE_NODE,
        match(node: ViewTreeNode): boolean {
            return node.name === 'Canvas';
        }
    };

    public registerMatchers(): MatcherCallback[] {
        const matchMethodCb: MatcherCallback = {
            matcher: this.vtnMatcher,
            callback: this.check
        };
        return [matchMethodCb];
    }

    public check = (node: ViewTreeNode): void => {
        const stmt = node.attributes.get('opacity')?.[0];
        if (!stmt) {
            return;
        }
        if (this.checkOpacity(stmt)) {
            const callback = node.attributes.get('onReady')?.[1]?.[0];
            const scene = stmt.getCfg().getDeclaringMethod().getDeclaringArkFile().getScene();
            if (callback instanceof MethodSignature && this.isUsedLottie(scene.getMethod(callback), scene)) {
                this.reportIssue(stmt, 'opacity'.length);
            }
        }
    };

    private checkOpacity(stmt: Stmt): boolean {
        let arg = stmt.getInvokeExpr()?.getArg(0);
        if (arg instanceof Local) {
            let stmts = stmt.getCfg().getStmts();
            for (const stmt1 of stmts) {
                if (stmt1 instanceof ArkAssignStmt && stmt1.getLeftOp() === arg && this.isOpacityZero(stmt1)) {
                    return true;
                }
            }
        } else if (arg instanceof Constant && arg.getValue() === '0') {
            return true;
        }
        return false;
    }

    private isOpacityZero(stmt: Stmt): boolean {
        if (!(stmt instanceof ArkAssignStmt)) {
            return false;
        }
        let rightOp = stmt.getRightOp();
        if (rightOp instanceof Constant && rightOp.getValue() === '0') {
            return true;
        } else if (rightOp instanceof ArkInstanceFieldRef) {
            const scene = stmt.getCfg().getDeclaringMethod().getDeclaringArkFile().getScene();
            let field = ModelUtils.findArkModelBySignature(rightOp.getFieldSignature(), scene);
            if (field instanceof ArkField) {
                let stmts = field.getInitializer();
                return this.isOpacityZero(stmts[stmts.length - 1]);
            }
        }
        return false;
    }

    private isUsedLottie(method: ArkMethod | null, scene: Scene, visited: Set<ArkMethod> = new Set()): boolean {
        if (!method || method.getDeclaringArkFile().getProjectName() !== scene.getProjectName()) {
            return false;
        }
        if (visited.has(method)) {
            return false;
        } else {
            visited.add(method);
        }
        const stmts = method.getCfg()?.getStmts();
        if (!stmts) {
            return false;
        }
        for (const s of stmts) {
            const methodSignature = s.getInvokeExpr()?.getMethodSignature();
            if (methodSignature) {
                if (loadAnimationSignatures.has(methodSignature?.toString())) {
                    return true;
                }
                if (this.isUsedLottie(scene.getMethod(methodSignature), scene, visited)) {
                    return true;
                }
            }
        }
        return false;
    }


    private reportIssue(stmt: Stmt, length: number): void {
        const filePath = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile().getFilePath();
        let length1 = stmt.getOperandOriginalPositions()?.length ?? 1;
        let fullPosition = stmt.getOperandOriginalPosition(length1 - 1);
        if (!fullPosition) {
            return;
        }
        let lineNum = fullPosition.getFirstLine();
        let endColumn = fullPosition.getFirstCol() - 1;
        let startColumn = endColumn - length;
        if (lineNum === -1 || startColumn === -1) {
            return;
        }
        const severity = this.rule.alert ?? this.metaData.severity;
        let defects = new Defects(lineNum, startColumn, endColumn, this.metaData.description, severity,
            this.rule.ruleId, filePath, this.metaData.ruleDocPath, true, false, false);
        this.issues.push(new IssueReport(defects, undefined));
    }
}